/* ******************************************************************************
 * Copyright (c) 2006-2013 XMind Ltd. and others.
 *
 * This file is a part of XMind 3. XMind releases 3 and
 * above are dual-licensed under the Eclipse Public License (EPL),
 * which is available at http://www.eclipse.org/legal/epl-v10.html
 * and the GNU Lesser General Public License (LGPL),
 * which is available at http://www.gnu.org/licenses/lgpl.html
 * See http://www.xmind.net/license.html for details.
 *
 * Contributors:
 *     XMind Ltd. - initial API and implementation
 *******************************************************************************/
package org.xmind.de.erichseifert.vectorgraphics2d;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

import org.xmind.de.erichseifert.vectorgraphics2d.util.DataUtils;
import org.xmind.de.erichseifert.vectorgraphics2d.util.GraphicsUtils;

/**
 * {@code Graphics2D} implementation that saves all operations to a string in
 * the <i>Portable Document Format</i> (PDF).
 *
 * @author Jason Wong
 */
public class PDFGraphics2D extends VectorGraphics2D {
    /** Prefix string for PDF font resource ids. */
    protected static final String FONT_RESOURCE_PREFIX = "F"; //$NON-NLS-1$
    /** Prefix string for PDF image resource ids. */
    protected static final String IMAGE_RESOURCE_PREFIX = "Im"; //$NON-NLS-1$
    /** Prefix string for PDF transparency resource ids. */
    protected static final String TRANSPARENCY_RESOURCE_PREFIX = "T"; //$NON-NLS-1$

    /**
     * Constant to convert values from millimeters to PostScript®/PDF units
     * (1/72th inch).
     */
    protected static final double MM_IN_UNITS = 72.0 / 25.4;

    /** Mapping of stroke endcap values from Java to PDF. */
    private static final Map<Integer, Integer> STROKE_ENDCAPS = DataUtils.map(
            new Integer[] { BasicStroke.CAP_BUTT, BasicStroke.CAP_ROUND,
                    BasicStroke.CAP_SQUARE }, new Integer[] { 0, 1, 2 });

    /** Mapping of line join values for path drawing from Java to PDF. */
    private static final Map<Integer, Integer> STROKE_LINEJOIN = DataUtils.map(
            new Integer[] { BasicStroke.JOIN_MITER, BasicStroke.JOIN_ROUND,
                    BasicStroke.JOIN_BEVEL }, new Integer[] { 0, 1, 2 });

    /** Id of the current PDF object. */
    private int curObjId;
    /** Mapping from objects to file positions. */
    private final Map<Integer, Integer> objPositions;
    /** Mapping from transparency levels to transparency resource ids. */
    private final Map<Double, String> transpResources;
    /** Mapping from image data to image resource ids. */
    private final Map<BufferedImage, String> imageResources;
    /** Mapping from font objects to font resource ids. */
    private final Map<Font, String> fontResources;
    /** File position of the actual content. */
    private int contentStart;

    private double scale;
    private double scaledOffsetX;
    private double scaledOffsetY;

    /**
     * Constructor that initializes a new {@code PDFGraphics2D} instance. The
     * document dimension must be specified as parameters.
     */
    public PDFGraphics2D(double x, double y, double width, double height,
            double scale, double scaledOffsetX, double scaledOffsetY) {
        super(x, y, width, height);
        this.scale = scale;
        this.scaledOffsetX = Math.round(scaledOffsetX * 100) / 100.0;
        this.scaledOffsetY = Math.round(scaledOffsetY * 100) / 100.0;
        curObjId = 1;
        objPositions = new TreeMap<Integer, Integer>();
        transpResources = new TreeMap<Double, String>();
        imageResources = new LinkedHashMap<BufferedImage, String>();
        fontResources = new LinkedHashMap<Font, String>();
        writeHeader();
    }

    @SuppressWarnings("nls")
    @Override
    protected void writeString(String str, double x, double y) {
        // Escape string
        str = str.replaceAll("\\\\", "\\\\\\\\").replaceAll("\t", "\\\\t")
                .replaceAll("\b", "\\\\b").replaceAll("\f", "\\\\f")
                .replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");

        float fontSize = getFont().getSize2D();
        float leading = getFont().getLineMetrics("", getFontRenderContext())
                .getLeading();

        // Start text and save current graphics state
        writeln("q BT");

        String fontResourceId = getFontResource(getFont());
        writeln("/", fontResourceId, " ", fontSize, " Tf");
        // Set leading
        writeln(fontSize + leading, " TL");

        // Undo swapping of y axis for text
        writeln("1 0 0 -1 ", x, " ", y, " cm");

        /*
         * // Extract lines String[] lines = str.replaceAll("\r\n",
         * "\n").replaceAll("\r", "\n").split("\n"); // Paint lines for (int i =
         * 0; i < lines.length; i++) { writeln("(", lines[i], ") ", (i == 0) ?
         * "Tj" : "'"); }
         */

        str = str.replaceAll("[\r\n]", "");
        writeln("(", str, ") Tj");

        // End text and restore previous graphics state
        writeln("ET Q");
    }

    @SuppressWarnings("nls")
    @Override
    public void setStroke(Stroke s) {
        super.setStroke(s);
        if (s instanceof BasicStroke) {
            BasicStroke bs = (BasicStroke) s;
            writeln(bs.getLineWidth(), " w");
            writeln(STROKE_LINEJOIN.get(bs.getLineJoin()), " j");
            writeln(STROKE_ENDCAPS.get(bs.getEndCap()), " J");
            writeln("[", DataUtils.join(" ", bs.getDashArray()), "] ",
                    bs.getDashPhase(), " d");
        }
    }

    @SuppressWarnings("nls")
    @Override
    protected void writeImage(Image img, int imgWidth, int imgHeight, double x,
            double y, double width, double height) {
        BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
        String imageResourceId = getImageResource(bufferedImg);
        // Save graphics state
        write("q ");
        // Take current transformations into account
        AffineTransform txCurrent = getTransform();
        if (!txCurrent.isIdentity()) {
            double[] matrix = new double[6];
            txCurrent.getMatrix(matrix);
            write(DataUtils.join(" ", matrix), " cm ");
        }
        // Move image to correct position and scale it to (width, height)
        write(width, " 0 0 ", height, " ", x - 2, " ", y - 2, " cm ");

        // Swap y axis
        write("1 0 0 -1 0 1 cm ");
        // Draw image
        write("/", imageResourceId, " Do ");
        // Restore old graphics state
        writeln("Q");
    }

    @SuppressWarnings("nls")
    @Override
    public void setColor(Color c) {
        if (c != null) {
            super.setColor(c);
            // Add a new graphics state to resources
            double a = c.getAlpha() / 255.0;
            String transpResourceId = getTransparencyResource(a);
            writeln("/", transpResourceId, " gs");
            double r = c.getRed() / 255.0;
            double g = c.getGreen() / 255.0;
            double b = c.getBlue() / 255.0;
            write(r, " ", g, " ", b, " rg ");
            writeln(r, " ", g, " ", b, " RG");
        }
    }

    @Override
    public void setClip(Shape clip) {
        if (getClip() != null) {
            writeln("Q");//$NON-NLS-1$
        }
        super.setClip(clip);
        if (getClip() != null) {
            writeln("q");//$NON-NLS-1$
            writeShape(getClip());
            writeln(" W n");//$NON-NLS-1$
        }
    }

    // TODO Correct transformations
    /*
     * @Override protected void setAffineTransform(AffineTransform tx) { if
     * (getTransform().equals(tx)) { return; } // Undo previous transforms if
     * (isTransformed()) { writeln("Q"); } // Set new transform
     * super.setAffineTransform(tx); // Write transform to document if
     * (isTransformed()) { writeln("q"); double[] matrix = new double[6];
     * getTransform().getMatrix(matrix); writeln(DataUtils.join(" ", matrix),
     * " cm"); } } //
     */

    @SuppressWarnings("nls")
    @Override
    protected void writeHeader() {
        Rectangle2D bounds = getBounds();
        int x = (int) Math.floor(bounds.getX());// * MM_IN_UNITS);
        int y = (int) Math.floor(bounds.getY());// * MM_IN_UNITS);
        int w = (int) Math.ceil(bounds.getWidth());// * MM_IN_UNITS);
        int h = (int) Math.ceil(bounds.getHeight());// * MM_IN_UNITS);

        writeln("%PDF-1.4");
        // Object 1
        writeObj("Type", "/Catalog", "Pages", "2 0 R");
        // Object 2
        writeObj("Type", "/Pages", "Kids", "[3 0 R]", "Count", "1");
        // Object 3
        writeObj("Type", "/Page", "Parent", "2 0 R", "MediaBox",
                String.format("[%d %d %d %d]", x, y, w, h), "Contents",
                "4 0 R", "Resources", "6 0 R");
        // Object 5
        writeln(nextObjId(size()), " 0 obj");
        writeDict("Length", "5 0 R");
        writeln("stream");
        contentStart = size();
        writeln("q");
        // Adjust page size and page origin
        writeln(scale, " 0 0 ", -scale, " " + scaledOffsetX + " ", h
                - scaledOffsetY, " cm");
    }

    /**
     * Write a PDF dictionary from the specified collection of objects. The
     * passed objects are converted to strings. Every object with odd position
     * is used as key, every object with even position is used as value.
     *
     * @param strs
     *            Objects to be written to dictionary
     */
    @SuppressWarnings("nls")
    protected void writeDict(Object... strs) {
        writeln("<<");
        for (int i = 0; i < strs.length; i += 2) {
            writeln("/", strs[i], " ", strs[i + 1]);
        }
        writeln(">>");
    }

    /**
     * Write a collection of elements to the document stream as PDF object. The
     * passed objects are converted to strings.
     *
     * @param strs
     *            Objects to be written to the document stream.
     * @return Id of the PDF object that was written.
     */
    protected int writeObj(Object... strs) {
        int objId = nextObjId(size());
        writeln(objId, " 0 obj"); //$NON-NLS-1$
        writeDict(strs);
        writeln("endobj"); //$NON-NLS-1$
        return objId;
    }

    /**
     * Returns the next PDF object id without incrementing.
     *
     * @return Next PDF object id.
     */
    protected int peekObjId() {
        return curObjId + 1;
    }

    /**
     * Returns a new PDF object id with every call.
     *
     * @param position
     *            File position of the object.
     * @return A new PDF object id.
     */
    private int nextObjId(int position) {
        objPositions.put(curObjId, position);
        return curObjId++;
    }

    /**
     * Returns the resource for the specified transparency level.
     *
     * @param a
     *            Transparency level.
     * @return A new PDF object id.
     */
    protected String getTransparencyResource(double a) {
        String name = transpResources.get(a);
        if (name == null) {
            name = String.format("%s%d", TRANSPARENCY_RESOURCE_PREFIX, //$NON-NLS-1$
                    transpResources.size() + 1);
            transpResources.put(a, name);
        }
        return name;
    }

    /**
     * Returns the resource for the specified image data.
     *
     * @param bufferedImg
     *            Image object with data.
     * @return A new PDF object id.
     */
    protected String getImageResource(BufferedImage bufferedImg) {
        String name = imageResources.get(bufferedImg);
        if (name == null) {
            name = String.format("%s%d", IMAGE_RESOURCE_PREFIX, //$NON-NLS-1$
                    imageResources.size() + 1);
            imageResources.put(bufferedImg, name);
        }
        return name;
    }

    /**
     * Returns the resource describing the specified font.
     *
     * @param font
     *            Font to be described.
     * @return A new PDF object id.
     */
    protected String getFontResource(Font font) {
        String name = fontResources.get(font);
        if (name == null) {
            name = String.format("%s%d", FONT_RESOURCE_PREFIX, //$NON-NLS-1$
                    fontResources.size() + 1);
            fontResources.put(font, name);
        }
        return name;
    }

    /**
     * Utility method for writing a tag closing fragment for drawing operations.
     */
    @Override
    protected void writeClosingDraw(Shape s) {
        writeln(" S"); //$NON-NLS-1$
    }

    /**
     * Utility method for writing a tag closing fragment for filling operations.
     */
    @Override
    protected void writeClosingFill(Shape s) {
        writeln(" f"); //$NON-NLS-1$
        if (!(getPaint() instanceof Color)) {
            super.writeClosingFill(s);
        }
    }

    /**
     * Utility method for writing an arbitrary shape to. It tries to translate
     * Java2D shapes to the corresponding PDF shape commands.
     */
    @SuppressWarnings("nls")
    @Override
    protected void writeShape(Shape s) {
        // TODO Correct transformations
        /*
         * if (s instanceof Line2D) { Line2D l = (Line2D) s; double x1 =
         * l.getX1(); double y1 = l.getY1(); double x2 = l.getX2(); double y2 =
         * l.getY2(); write(x1, " ", y1, " m ", x2, " ", y2, " l"); } else if (s
         * instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; double x =
         * r.getX(); double y = r.getY(); double width = r.getWidth(); double
         * height = r.getHeight(); write(x, " ", y, " ", width, " ", height,
         * " re"); } else //
         */
        {
            s = getTransform().createTransformedShape(s);
            PathIterator segments = s.getPathIterator(null);
            double[] coordsCur = new double[6];
            double[] pointPrev = new double[2];
            for (int i = 0; !segments.isDone(); i++, segments.next()) {
                if (i > 0) {
                    write(" ");
                }
                int segmentType = segments.currentSegment(coordsCur);
                switch (segmentType) {
                case PathIterator.SEG_MOVETO:
                    write(coordsCur[0], " ", coordsCur[1], " m");
                    pointPrev[0] = coordsCur[0];
                    pointPrev[1] = coordsCur[1];
                    break;
                case PathIterator.SEG_LINETO:
                    write(coordsCur[0], " ", coordsCur[1], " l");
                    pointPrev[0] = coordsCur[0];
                    pointPrev[1] = coordsCur[1];
                    break;
                case PathIterator.SEG_CUBICTO:
                    write(coordsCur[0], " ", coordsCur[1], " ", coordsCur[2],
                            " ", coordsCur[3], " ", coordsCur[4], " ",
                            coordsCur[5], " c");
                    pointPrev[0] = coordsCur[4];
                    pointPrev[1] = coordsCur[5];
                    break;
                case PathIterator.SEG_QUADTO:
                    double x1 = pointPrev[0] + 2.0 / 3.0
                            * (coordsCur[0] - pointPrev[0]);
                    double y1 = pointPrev[1] + 2.0 / 3.0
                            * (coordsCur[1] - pointPrev[1]);
                    double x2 = coordsCur[0] + 1.0 / 3.0
                            * (coordsCur[2] - coordsCur[0]);
                    double y2 = coordsCur[1] + 1.0 / 3.0
                            * (coordsCur[3] - coordsCur[1]);
                    double x3 = coordsCur[2];
                    double y3 = coordsCur[3];
                    write(x1, " ", y1, " ", x2, " ", y2, " ", x3, " ", y3, " c");
                    pointPrev[0] = x3;
                    pointPrev[1] = y3;
                    break;
                case PathIterator.SEG_CLOSE:
                    write("h");
                    break;
                default:
                    throw new IllegalStateException("Unknown path operation.");
                }
            }
        }
    }

    /**
     * Returns a string which represents the data of the specified image.
     *
     * @param bufferedImg
     *            Image to convert.
     * @return String representation of image in PDF hexadecimal format.
     */
    private String getPdf(BufferedImage bufferedImg) {
        int width = bufferedImg.getWidth();
        int height = bufferedImg.getHeight();
        int bands = bufferedImg.getSampleModel().getNumBands();
        StringBuffer str = new StringBuffer(width * height * bands * 2);
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = bufferedImg.getRGB(x, y) & 0xffffff;
                if (bands >= 3) {
                    String hex = String.format("%06x", pixel); //$NON-NLS-1$
                    str.append(hex);
                } else if (bands == 1) {
                    str.append(String.format("%02x", pixel)); //$NON-NLS-1$
                }
            }
            str.append('\n');
        }
        return str.append('>').toString();
    }

    @SuppressWarnings("nls")
    @Override
    protected String getFooter() {
        StringBuffer footer = new StringBuffer();
        // TODO Correct transformations
        /*
         * if (isTransformed()) { footer.append("Q\n"); }
         */
        if (getClip() != null) {
            footer.append("Q\n"); //$NON-NLS-1$
        }
        footer.append("Q"); //$NON-NLS-1$
        int contentEnd = size() + footer.length();
        footer.append('\n');
        footer.append("endstream\n"); //$NON-NLS-1$
        footer.append("endobj\n");

        int lenObjId = nextObjId(size() + footer.length());
        footer.append(lenObjId).append(" 0 obj\n");
        footer.append(contentEnd - contentStart).append('\n');
        footer.append("endobj\n");

        int resourcesObjId = nextObjId(size() + footer.length());
        footer.append(resourcesObjId).append(" 0 obj\n");
        footer.append("<<\n");
        footer.append(" /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n");

        // Add resources for fonts
        if (!fontResources.isEmpty()) {
            footer.append(" /Font <<\n");
            for (Map.Entry<Font, String> entry : fontResources.entrySet()) {
                Font font = entry.getKey();
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId)
                        .append(" << /Type /Font").append(" /Subtype /")
                        .append("TrueType").append(" /BaseFont /")
                        .append(font.getPSName()).append(" >>\n");
            }
            footer.append(" >>\n");
        }

        // Add resources for images
        if (!imageResources.isEmpty()) {
            footer.append(" /XObject <<\n");

            int objIdOffset = 0;
            for (Map.Entry<BufferedImage, String> entry : imageResources
                    .entrySet()) {
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId).append(' ')
                        .append(curObjId + objIdOffset).append(" 0 R\n");
                objIdOffset++;
            }
            footer.append(" >>\n");
        }

        // Add resources for transparency levels
        if (!transpResources.isEmpty()) {
            footer.append(" /ExtGState <<\n");
            for (Map.Entry<Double, String> entry : transpResources.entrySet()) {
                Double alpha = entry.getKey();
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId)
                        .append(" << /Type /ExtGState").append(" /ca ")
                        .append(alpha).append(" /CA ").append(alpha)
                        .append(" >>\n");
            }
            footer.append(" >>\n");
        }

        footer.append(">>\n");
        footer.append("endobj\n");

        // Add data of images
        for (BufferedImage image : imageResources.keySet()) {
            int imageObjId = nextObjId(size() + footer.length());
            footer.append(imageObjId).append(" 0 obj\n");
            footer.append("<<\n");
            String imageData = getPdf(image);
            footer.append("/Type /XObject\n").append("/Subtype /Image\n")
                    .append("/Width ").append(image.getWidth()).append('\n')
                    .append("/Height ").append(image.getHeight()).append('\n')
                    .append("/ColorSpace /DeviceRGB\n")
                    .append("/BitsPerComponent 8\n").append("/Length ")
                    .append(imageData.length()).append('\n')
                    .append("/Filter /ASCIIHexDecode\n").append(">>\n")
                    .append("stream\n").append(imageData)
                    .append("\nendstream\n").append("endobj\n");
        }

        int objs = objPositions.size() + 1;

        int xrefPos = size() + footer.length();
        footer.append("xref\n");
        footer.append("0 ").append(objs).append('\n');
        // lines of xref entries must must be exactly 20 bytes long
        // (including line break) and thus end with <SPACE NEWLINE>
        footer.append(String.format("%010d %05d", 0, 65535)).append(" f \n");
        for (int pos : objPositions.values()) {
            footer.append(String.format("%010d %05d", pos, 0)).append(" n \n");
        }

        footer.append("trailer\n");
        footer.append("<<\n");
        footer.append("/Size ").append(objs).append('\n');
        footer.append("/Root 1 0 R\n");
        footer.append(">>\n");
        footer.append("startxref\n");
        footer.append(xrefPos).append('\n');

        footer.append("%%EOF\n");
        return footer.toString();
    }

    @SuppressWarnings("nls")
    public void writeFooter(Writer out) throws IOException {
        StringBuffer footer = new StringBuffer();

        if (getClip() != null) {
            footer.append("Q\n");
        }
        footer.append("Q");
        int contentEnd = size() + footer.length();
        footer.append('\n');
        footer.append("endstream\n");
        footer.append("endobj\n");

        int lenObjId = nextObjId(size() + footer.length());
        footer.append(lenObjId).append(" 0 obj\n");
        footer.append(contentEnd - contentStart).append('\n');
        footer.append("endobj\n");

        int resourcesObjId = nextObjId(size() + footer.length());
        footer.append(resourcesObjId).append(" 0 obj\n");
        footer.append("<<\n");
        footer.append(" /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n");
        out.write(footer.toString());
        footer = new StringBuffer();

        // Add resources for fonts
        if (!fontResources.isEmpty()) {
            footer.append(" /Font <<\n");
            for (Map.Entry<Font, String> entry : fontResources.entrySet()) {
                Font font = entry.getKey();
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId)
                        .append(" << /Type /Font").append(" /Subtype /")
                        .append("TrueType").append(" /BaseFont /")
                        .append(font.getPSName()).append(" >>\n");
            }
            footer.append(" >>\n");
        }

        // Add resources for images
        if (!imageResources.isEmpty()) {
            footer.append(" /XObject <<\n");
            int objIdOffset = 0;
            for (Map.Entry<BufferedImage, String> entry : imageResources
                    .entrySet()) {
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId).append(' ')
                        .append(curObjId + objIdOffset).append(" 0 R\n");
                objIdOffset++;
            }
            footer.append(" >>\n");
        }

        // Add resources for transparency levels
        if (!transpResources.isEmpty()) {
            footer.append(" /ExtGState <<\n");
            for (Map.Entry<Double, String> entry : transpResources.entrySet()) {
                Double alpha = entry.getKey();
                String resourceId = entry.getValue();
                footer.append("  /").append(resourceId)
                        .append(" << /Type /ExtGState").append(" /ca ")
                        .append(alpha).append(" /CA ").append(alpha)
                        .append(" >>\n");
            }
            footer.append(" >>\n");
        }

        footer.append(">>\n");
        footer.append("endobj\n");
        out.write(footer.toString());
        footer = new StringBuffer();

        // Add data of images
        for (BufferedImage image : imageResources.keySet()) {
            int imageObjId = nextObjId(size() + footer.length());
            footer.append(imageObjId).append(" 0 obj\n");
            footer.append("<<\n");
            String imageData = getPdf(image);
            footer.append("/Type /XObject\n").append("/Subtype /Image\n")
                    .append("/Width ").append(image.getWidth()).append('\n')
                    .append("/Height ").append(image.getHeight()).append('\n')
                    .append("/ColorSpace /DeviceRGB\n")
                    .append("/BitsPerComponent 8\n").append("/Length ")
                    .append(imageData.length()).append('\n')
                    .append("/Filter /ASCIIHexDecode\n").append(">>\n")
                    .append("stream\n");
            out.write(footer.toString());
            out.write(imageData);
            out.write("\nendstream\n");
            out.write("endobj\n");
            footer = new StringBuffer();
        }

        int objs = objPositions.size() + 1;

        int xrefPos = size() + footer.length();
        footer.append("xref\n");
        footer.append("0 ").append(objs).append('\n');
        // lines of xref entries must must be exactly 20 bytes long
        // (including line break) and thus end with <SPACE NEWLINE>
        footer.append(String.format("%010d %05d", 0, 65535)).append(" f \n");
        for (int pos : objPositions.values()) {
            footer.append(String.format("%010d %05d", pos, 0)).append(" n \n");
        }

        footer.append("trailer\n");
        footer.append("<<\n");
        footer.append("/Size ").append(objs).append('\n');
        footer.append("/Root 1 0 R\n");
        footer.append(">>\n");
        footer.append("startxref\n");
        footer.append(xrefPos).append('\n');

        footer.append("%%EOF\n");
        out.write(footer.toString());
    }

    @Override
    public String toString() {
        // String doc = super.toString();
        String doc = super.getDocument().toString();
        // doc =
        // doc.replaceAll("q\n[0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* [0-9]+\\.?[0-9]* cm\nQ\n",
        // "");
        return doc;
    }

    @Override
    public byte[] getBytes() {
        try {
            return toString().getBytes("UTF-8"); //$NON-NLS-1$
        } catch (UnsupportedEncodingException e) {
            return super.getBytes();
        }
    }
}
